(function($) {

/**
 * Compiled scripts are stored in this collection for later use. Calls to 
 * <code>$.bumble</code> will will look here first. Scripts are stored using 
 * their expanded, canonical URLs as keys.
 */
var cache = { };


/**
 * Executes the Bumble template from the specified URL. The resulting HTML is 
 * injected into the matching elements of the current jQuery object, using
 * the <code>.html()</code> method.
 * 
 * @param {String} url The URL of the template that should be executed.
 * @param {Object} params The keys and values in this object will be 
 *                 available, as variables, within the template's
 *                 execution scope.
 * @param {Object} options Specifies the options by which the template
 *                 will be downloaded and executed.
 */
$.fn.bumble = function(url, params, options) {
    var elements = this;
    $.fn.bumble.run(url, params, options, function(result) {
        elements.html(result);
    });
}  


/**
 * Executes the Bumble template from the specified URL. If this method is
 * called synchronously (by setting <code>async</code> to <code>false</code>
 * in the <code>options</code> argument), this method will return the 
 * resulting HTML. Otherwise this method will, when finished, trigger the 
 * <code>callback</code> argument, passing the resulting HTML as its only
 * parameter.
 *
 * @param {String} url The URL of the template that should be executed.
 * @param {Object} params The keys and values in this object will be 
 *                 available, as variables, within the template's
 *                 execution scope.
 * @param {Object} options Specifies the options by which the template
 *                 will be downloaded and executed.
 * @param {Function} callback A callback function that will be triggered
 *                 when the template is downloaded and executed. It accepts
 *                 a single argument, which will be the resulting HTML.
 */
$.fn.bumble.run = function(url, params, options, callback) {
    var options = $.extend({ }, $.fn.bumble.defaults, options || { });
    var params = params || { };
    var output = null;
    loadTemplate(url, options, function(script) {
        output = script(params);
        if (callback) {
            callback(output);
        }
    });
    return output;
}


/**
 * Loads the contents of a template, caches it, and returns the contents
 * via the supplied callback. If there are any <code>preload</code> statements
 * within the template, those will be loaded as well, before the callback
 * is triggered.
 *
 * @param {String} url The URL of the template to be loaded.
 * @param {Object} options Specifies the options by which the template
 *                 will be downloaded. 
 * @param {Function} callback A callback function that will be triggered
 *                 when the template is downloaded. It accepts a single
 *                 argument, which will be the compiled script's method.
 */
function loadTemplate(url, options, callback) {
    var canonical = getCanonicalURL(url);
    if (cache[canonical]) {
        callback(cache[canonical]);
    }
    else {    
        $.ajax({ url: url, async: options.async, success: function(data) {
            var script = compileScript(data, url);
            if (script) {
                cache[canonical] = script;
                var imports = [ ]                
                $.each(script.imports, function(i, importURL) {
                    imports.push((getCanonicalURL(importURL, getCanonicalURL(url))));
                });
                loadMultipleTemplates(imports, options, function() {
                    callback(script); 
                });
            }
        }});
    }
}


/**
 * Loads multiple template files, identically to <code>loadTemplate</code>, 
 * but doesn't trigger its callback until all are fully loaded.
 *
 * @param {Array}  urls The URLs of the templates to be loaded.
 * @param {Object} options Specifies the options by which the templates
 *                 will be downloaded. 
 * @param {Function} callback A callback function that will be triggered
 *                 when all the templates are downloaded. It accepts no 
 *                 arguments
 */
function loadMultipleTemplates(urls, options, callback) {
    var pending = 1;
    function check() { 
        if (--pending == 0) {
            callback();
        }
    }
    $.each(urls, function(i, url) {
        if (!cache[url]) {
            pending++;
            loadTemplate(url, options, check);
        } 
    });
    check();
}


/** 
 * Encode a XML characters in a string.
 *
 * @param {String} text The text to be encoded.
 */
function encode(text) {
    return text.replace(/&/g, "&amp;")
		.replace(/>/g, "&gt;")
		.replace(/</g, "&lt;")
		.replace(/"/g, "&quot;")
		.replace(/'/g, "&apos;");
}


/**
 * Wraps a string in single-quotes, safely escaping the contents
 * for use in JavaScript.
 *
 * @param {String} text The text to be quoted.
 */
function singleQuote(text) {
    return "'" + text.replace(/\\/g, "\\\\").
		replace(/'/g, "\\'").
		replace(/\n/g, "\\n") + "'";
}


/**
 * A safe wrapper around the <code>console.error</code> method that's
 * offered by many, but not all, browsers. On browsers where it isn't 
 * supported, this method will do nothing.
 *
 * @param {String} message The message to be traced using the console.
 */
function traceError(message) {
    try {
        console.error(message);
    }
    catch (e) {
        
    }
}


/**
 * Normalize the URL for a given path, resolving variable path elements
 * such as .. and ., and removing extra / characters.
 *
 * @param {String} url A URL.
 */
function normalizeURL(url) {    
    var SERVER_INFO	= 0;	
    var ROOT = 1;
    var CURRENT = 2;
    var PARENT = 3;
    var FOLDER = 4;
    var FILE = 5;
    var normal = [ ];
    var serverMatch = url.match(/^([a-z]+:\/\/[^\/]*)/i);
    if (serverMatch) {
        normal.push({ type: SERVER_INFO, value: serverMatch[1] });
        url = url.replace(/^([a-z]+:\/\/[^\/]*)/i, "");
    }    
    url = url.replace(/\/+/g, "/");
    if (url[0] == "/") {
        normal.push({ type: ROOT, value: "" });
        url = url.replace(/^\//, "");
    }    
    var parts = url.split("/");
    for (var i = 0; i < parts.length; i++) {
		var parentType = (normal.length > 0) ? normal[normal.length - 1].type : null;
        if (parts[i] == "..") {
			if (normal.length > 0 && parentType != PARENT && parentType != SERVER_INFO && parentType != ROOT) {
				normal.pop();
			}
			else {
            	normal.push({ type: PARENT, value: ".." });
        	}
        }
        else if (i == (parts.length - 1)) {
            normal.push({ type: FILE, value: parts[i] });
            break;
        }
        else if (parts[i] != "." && !(i == (parts.length - 1) && parts[parts.length - 1] == "")) {
            normal.push({ type: FOLDER, value: parts[i] });
        }
    }
    for (var i = 0, url = ""; i < normal.length; i++) {
        url += normal[i].value;
        if(normal[i].type != FILE && normal[i].type != SERVER_INFO) {
            url += "/";
        }
    }
    return url;
}


/**  
 * Resolves the location of one relative URL path relative to a base URL.
 *
 * @param {String} url A relative URL
 * @param {String} base The base URL that the relative path should be resolved
 *                 from.
 */ 
function getCanonicalURL(url, base) {
    base = base || window.location.href;
    if (url.match(/^[a-z]+:\/\/.+/i)) {
        return normalizeURL(url);
    }
    else if (base.match(/^[a-z]+:\/\/[^\/]*$/i)) {
        base += "/";
    }
    else if (base.match(/^[^\/]+$/)) {
        base = '';
    }
    else {
        base = base.replace(/\/[^\/]*$/, "/");
    }    
    if (url[0] == "/" && base.match(/^[a-z]+:\/\/.+/i)) {
		return normalizeURL(base.replace(/^([a-z]+:\/\/[^\/]*)\/.*/i, "$1" + url));
	}
    else if(url[0] == "/") {
    	return normalizeURL(url);
    }
    else {
		return normalizeURL(base + url);
	}
}


/**
 * Executes a compiled Bumble script using the provided scope.
 *
 * @param {Function} compiled The compiled script method.
 * @param {Object} scope A collection of variables that will be imported
 *                 into the scope of the executing script. 
 */
function executeScript(compiled, scope) {
    var scopeImportStatements = [ ];
    for (prop in scope) {
        if (prop.match(/^[a-z][a-z0-9_]*$/i)) {
            scopeImportStatements.push("var " + prop + " = scope['" + prop + "'];\n");
        }
    }
    eval(scopeImportStatements.join(''));
    var run = function(u,p){ __p($.fn.bumble.run(u,p,{async:false})) };
    var __o = [ ];
	var __p = function(s){ __o.push(s && s.toString()) };
	var __e = function(s){ __p(encode(s.toString())) };
	var echo = __p;
	try {
	    (function(){ eval(compiled); })();
	}
	catch (e) {
	    traceError("An error occurred in your Bumble script: " + e.toString());
	}
	return __o.join('');
}


/** 
 * Compiles Bumble script into a JavaScript function.
 *
 * @param {String} source The uncompiled source for this Bumble template.
 */
function compileScript(source) {
	var start = "<%";	
	var end   = "%>";
    var echo = "=";
	var meta = "@";
	var compiled = [ ];
	var scratch = "";
	var echoing = true;
	var i = 0;
	var imports = [ ];
	while (i < source.length) {
		if (source.substr(i, start.length) == start) {
			var escaped = false;
			var quoted = false;
			var delimiter = "";
			i += start.length;
			if (echoing && scratch.length > 0) {
				compiled.push("__p(" + singleQuote(scratch) + ");");
			}
			echoing = false;
			scratch = "";
			while (i < source.length) {
				if (!quoted && source.substr(i, end.length) == end) {
					if (scratch.substr(0, echo.length) == echo) {
						scratch = "__p(" + scratch.substr(echo.length) +  ");";
					}
					else if (scratch.substr(0, meta.length) == meta) {
						var sMatch = scratch.match(/\s*preload="([^"]*)"/i);
						if (sMatch) {
							imports.push(sMatch[1]);							
						    scratch = '';
						}
					}
					else if (scratch.match(/^\s*(end|endforeach|endif|endwhile)\s*$/i)) {
				        scratch = '}';
				    }				    
				    else {
                        scratch = scratch.replace(/:\s*$/, '{');
				    }
					compiled.push(scratch);
					scratch = "";
					i += end.length;
					break;
				}
				else {
					var current = source.charAt(i);
					if (!quoted && (current == '"' || current == "'")) {
						quoted = true;
						delimiter = current ;
					}
					else if (quoted && !escaped && current == delimiter) {
						quoted = false;
					}					
					escaped = (!escaped && current == "\\");
					scratch += current;
					i++;
				}
			}
		}
		else {
			echoing = true;
			scratch += source.charAt(i);
			i++;
		}
	}
	if (echoing && scratch.length > 0) {
		compiled.push("__p(" + singleQuote(scratch) + ");");
	}
	var result = function(scope) { return executeScript(compiled.join("\n"), scope); };
	result.imports = imports;
	return result;
}


/**
 * Publicly-exposed defaults for this jQuery plugin.
 */
$.fn.bumble.defaults = {
    async: true
}    
  
})(jQuery);